import { EventEmitter } from '../common/event-emmiter';
import { Newable } from '../utilities/newable';
import { IWorld } from './common/world.interface';

import { Component } from './components/component';
import { EventComponent } from './components/event.component';
import { Entity } from './entities/entity';
import { System } from './systems/system';

export class World implements IWorld {
  #systems: System[] = [];
  #entities: Map<Entity, Component[]> = new Map();

  readonly onLoading = new EventEmitter<boolean>();

  findEntitiesByComponent(componentClass: Newable<Component>): Entity[] {
    return this.findEntitiesByComponents([componentClass]);
  }

  findEntitiesByComponents(componentClasses: Newable<Component>[]): Entity[] {
    const entities: Entity[] = [];

    for (const [entity, components] of this.#entities) {
      if (
        componentClasses.every((componentClass) => components.some((component) => component instanceof componentClass))
      ) {
        entities.push(entity);
      }
    }

    return entities;
  }

  findComponentByEntity<T extends Component>(entity: Entity, componentClass: Newable<T>): T | null {
    const components = this.findComponentsByEntity(entity);
    return (components.find((c) => c instanceof componentClass) as T) ?? null;
  }

  findComponentsByEntity(entity: Entity): Component[] {
    return [...(this.#entities.get(entity) ?? [])];
  }

  addEntity(entity: Entity) {
    const components = this.#entities.get(entity);
    if (!components) {
      this.#entities.set(entity, []);
    }
  }

  removeEntity(entity: Entity) {
    const components = this.#entities.get(entity);
    if (components) {
      this.#entities.delete(entity);
    }
  }

  attachComponentToEntity<T extends Component>(componentClass: Newable<T>, componentInstance: T, entity: Entity) {
    const components = this.#entities.get(entity);
    if (!components) {
      throw new Error(`Entity '${entity.description}' does not exist`);
    }

    if (components.findIndex((currentComponent) => currentComponent instanceof componentClass) !== -1) {
      throw new Error(`Component '${componentClass.name}' is already attached to entity '${entity.description}'`);
    }

    components.push(componentInstance);
  }

  detachComponentFromEntity(componentClass: Newable<Component>, entity: Entity) {
    const components = this.#entities.get(entity);
    if (!components) {
      throw new Error(`Entity '${entity.description}' does not exist`);
    }

    const foundIndex = components.findIndex((currentComponent) => currentComponent instanceof componentClass);
    if (foundIndex === -1) {
      throw new Error(`Component '${componentClass.name}' is not attached to entity '${entity.description}'`);
    }

    components.splice(foundIndex, 1);
  }

  pushEvent<T extends EventComponent>(componentClass: Newable<T>, componentInstance: T) {
    const entity = new Entity('Event');
    this.addEntity(entity);
    this.attachComponentToEntity(componentClass, componentInstance, entity);
  }

  addSystem(system: System) {
    if (this.#systems.includes(system)) {
      return;
    }

    this.#systems.push(system);
  }

  removeSystem(system: System) {
    const foundIndex = this.#systems.findIndex((currentSystem) => currentSystem === system);

    if (foundIndex === -1) {
      return;
    }

    this.#systems.splice(foundIndex, 1);
  }

  async start() {
    this.onLoading.emit(true);

    await this.preload();
    await this.load();

    this.onLoading.emit(false);

    for (const system of this.#systems) {
      system.onStart(this);
    }
  }

  async preload() {
    /// hook to load assets
  }

  async load() {
    /// hook to load world
  }

  update(dt: number) {
    for (const system of this.#systems) {
      system.onUpdate(this, dt);
    }
  }

  postUpdate(dt: number) {
    this.cleanUpEvents();
  }

  finish() {
    this.#systems.forEach((system) => system.onFinish());
    this.#systems.splice(0, this.#systems.length);
    this.#entities.clear();
  }

  private cleanUpEvents() {
    const entities = this.findEntitiesByComponent(EventComponent);

    entities.forEach((entity) => {
      this.detachComponentFromEntity(EventComponent, entity);

      const components = this.findComponentsByEntity(entity);
      if (components.length === 0) {
        this.removeEntity(entity);
      }
    });
  }
}
